#region License
/* ---------------------------------------------------------------- *
 *
 * This file is part of the Xcoordination Application Space
 * ("XcoAppSpace") http://www.xcoordination.com
 *
 * Copyright (C) 2009 Xcoordination http://www.xcoordination.com
 *
 * XcoAppSpace is free software; you can redistribute it and/or
 * modify it under the terms of version 2 of the GNU General
 * Public License as published by the Free Software Foundation.
 *
 * XcoAppSpace is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program. If not, see http://www.gnu.org/licenses/
 * or write to the Free Software Foundation, Inc., 51 Franklin
 * Street, Fifth Floor, Boston, MA 02110-1301, USA.
 *
 * ---------------------------------------------------------------- */
#endregion

using System;
using System.Collections.Generic;
using System.Collections;
using System.Text;
using System.ServiceModel;
using System.ServiceModel.Configuration;
using System.ServiceModel.Description;
using System.Configuration;
using System.Net;
using System.Threading;
using XcoAppSpaces.Contracts.Transport;
using XcoAppSpaces.Contracts.Exceptions;
using XcoAppSpaces.Contracts.Logging;
using XcoAppSpaces.Contracts.Serialization;
using System.Linq;

namespace XcoAppSpaces.Transport.WCF
{
    /// <summary>
    /// Service that manages incoming and outgoing messages from/to remote spaces by use of WCF. Incoming
    /// messages are received by a hosted SpacePortal service. Outgoing messages are sent by RemoteSender 
    /// objects, the currently open RemoteSenders are managed in a list. A Timer regularly checks if any 
    /// connections to remote spaces have run into a timeout, and closes and removes such connections.
    /// </summary>
    public class XcoWCFTransportService : IXcoTransportService, IXcoWCFTransportSessionShutdown
    {
		/// <summary>
		/// Event that is raised when a message is received.
		/// </summary>
		public event MessageReceivedDelegate OnMessageReceived;

        /// <summary>
        /// Load different WCF-Configurations for the servicehost.
        /// </summary>
        public LoadHostConfigurationDelegate ConfigLoader
        {
            get { return loadHostConfig; }
            set { loadHostConfig = value; }
        }
        /// <summary>
        /// Standard config string for the wcf.config file
        /// </summary>
        private static string standardConfig = "XcoService";
        /// <summary>
        /// Load different RemoteSender instances
        /// </summary>
        public NewRemoteSenderDelegate CreateNewRemoteSender
        {
            get { return createNewRemoteSender; }
            set { createNewRemoteSender = value; }
        }


        private LoadHostConfigurationDelegate loadHostConfig = null;
        private NewRemoteSenderDelegate createNewRemoteSender = null;
        /// <summary>
        /// Supports different host-configurations
        /// </summary>
        /// <param name="coreUri">Uri from core</param>
        /// <param name="spacePortal"></param>
        /// <returns></returns>
        public delegate ServiceHost LoadHostConfigurationDelegate(Uri coreUri, IRemoteSpaceService spacePortal);

        /// <summary>
        /// Supports different constructors
        /// </summary>
        /// <param name="addr"></param>
        /// <returns></returns>
        public delegate RemoteSender NewRemoteSenderDelegate(string addr);

        private IRemoteSpaceService spacePortal;
        private ServiceHost serviceHost;
        private IBindingsHelper bindingHelper;

        /// <summary>
        /// Set the BindingHelper for additional functionalities
        /// </summary>
        /// <param name="bindingHelper"></param>
        protected void SetBindingHelper(IBindingsHelper bindingHelper)
        {
            this.bindingHelper = bindingHelper;
        }

    
        /// <summary>
        /// External configuration group
        /// </summary>
        private ServiceModelSectionGroup smsGroup;
        
        /// <summary>
        /// Set space instance and WCF configuration
        /// </summary>
        /// <param name="newService">External IRemoteSpaceService instance, e.g. from IIS hosting</param>
        /// <param name="group">Configurationsection for WCF settings</param>
        public void SetSpaceService(IRemoteSpaceService newService, ServiceModelSectionGroup group)
        {
            if (spacePortal == null)
            {
                spacePortal = newService;
                smsGroup = group;
                running = true;
                bindingHelper.Convert(new WSHttpBinding(),address.ToString());

            }
            else
                throw new XcoCommunicationException("Service already created.");
        }

        private Uri address;
        //private String hostAddress;
        //private int port = 0;
        //private String hostname = null;
        //private ResponseReceivedMethod processResponse; //needed for response when RequestMessage cannot be sent
        private ManualResetEvent waitForConnect; //event for service host to be opened
        private XcoCommunicationException connectError = null; //to store an error that happened when opening the service host, so it can be thrown after waiting
        private ManualResetEvent waitForStop; //event to wait until the service host should be closed.
        private bool running = false;

        private const int timeoutCheckInterval = 30000; //check for connection timeouts every 30 seconds
        private const int connectionTimeout = 60; //60 seconds connection timeout
        private Timer timeoutCheckTimer; //the timer that triggers the timeout checks

        //remote senders are managed in a dictionary so they can be found very fast (remote address is the key)
        private Dictionary<String, RemoteSender> remoteSenders = new Dictionary<string, RemoteSender>();
        private IXcoLogger log = null;


        /// <summary>
        /// Name for this instance on the used port
        /// </summary>
        public string SharedPortName { get; set; }



        /// <summary>
        /// Creates a new XcoWCFTransportService.
        /// </summary>
        /// <param name="port">The port that should be used (net.tcp).</param>
        public XcoWCFTransportService(int port)
        {
            Init();
			GenerateAddrUri(port);
        }
        /// <summary>
        /// Creates a new XcoWCFTransportService.
        /// </summary>
        /// <param name="address">The address that should be used (binding-specific).</param>
        public XcoWCFTransportService(string address)
        {
            Init();
			this.address = new Uri(address);
        }
        /// <summary>
        /// Generate address uri from port definition
        /// </summary>
        /// <param name="port"></param>
        private void GenerateAddrUri(int port)
        {
            if (address == null)
            {
                address = bindingHelper.GenerateStandardNetTcp(port);
            }
            else
            {
                UriBuilder uriB = new UriBuilder();
                uriB.Scheme = address.Scheme;
                uriB.Host = address.Host;
                uriB.Port = port;
                uriB.Path = address.AbsolutePath;
                address = uriB.Uri;
            }

        }

        /// <summary>
        /// Creates a new RemoteServer.
        /// </summary>
        public XcoWCFTransportService()
        {
            Init();
        }


        private void Init()
        {
			this.log = XcoLoggerFactory.CreateInstance(this.GetType());
            loadHostConfig = new LoadHostConfigurationDelegate(LoadConfiguration);
            createNewRemoteSender = new NewRemoteSenderDelegate(NewRemoteSender);
            bindingHelper = new BindingsHelper();
        }

        /// <summary>
        /// The port on which the service host is running.
        /// </summary>
        public int Port
        {
            get { return bindingHelper.Port(address); }
            set
            {
                if (!running)
                {
                    GenerateAddrUri(value);
                }
                else
                    throw new XcoCommunicationException("Port cannot be set while the communication service is running.");
            }
        }

        /// <summary>
        /// The Address of the Server.
        /// </summary>
        public string LocalAddress
        {
            get
            {
                string addr = bindingHelper.ServerAddr(address);
                if (!string.IsNullOrEmpty(SharedPortName) && !string.IsNullOrEmpty(addr))
                {
                    if (addr.IndexOf(SharedPortName,
                                     StringComparison.CurrentCultureIgnoreCase) < 0)
                    {//shared port not alread added
                        addr += "/" + SharedPortName;
                    }
                }
                return addr;
            }
        }

		/// <summary>
		/// Initializes the transport service.
		/// </summary>
		/// <param name="serviceRegistry">Service registry with other services of the space instance.</param>
		public void Initialize(XcoAppSpaces.Contracts.Service.IXcoServiceRegistry serviceRegistry)
		{
		}

    	/// <summary>
        /// Starts the service host in an own Thread, and waits until the service host is opened. 
        /// An exception is thrown if an error occured while opening the service host.
        /// </summary>
        public void Start()
        {
            if (spacePortal == null)
            {
                //if (log == null)
                //	log = LoggerFactory.CreateInstance("XcoSpaces.Services.Kernel.Communication", this.GetType());

                if (this.Port == 0)
                {
                    //if port = 0, generate a random port number between 8000 and 9000
                    //(only relevant if netTcp binding is used)
                    this.Port = CreateRandomPort();
                }

                spacePortal = new SpacePortal(address, this, log);

                //hostname = System.Environment.MachineName.ToLower();
                //hostAddress = hostname + ":" + port;
                waitForStop = new ManualResetEvent(false);

                //start service host in a new thread and wait for the service host to be opened
                waitForConnect = new ManualResetEvent(false);
                Thread t = new Thread(RunServiceHost);
                t.Start();
                waitForConnect.WaitOne();
                if (connectError != null)
                    throw connectError; //throw exception if there was an error while opening the service host

            }
    	    //start timeout check timer
            timeoutCheckTimer = new Timer(new TimerCallback(CheckConnectionTimeouts), null, timeoutCheckInterval, timeoutCheckInterval);
        }

		/// <summary>
		/// The start priority of the service.
		/// </summary>
		public int StartPriority
		{
			get { return 5; }
		}

        /// <summary>
        /// Creates Random-Port for the WCF-Service
        /// </summary>
        /// <returns></returns>
        private int CreateRandomPort()
        {
            return 8000 + (new Random()).Next(1000);
        }

        /// <summary>
        /// Load wcf-endpoint configuration from config-file, if exists
        /// </summary>
        /// <returns>null if no configuration is found</returns>
        private static ServiceEndpointElement LoadEndPointConfig()
        {
            //load configuration from the standard-exe-config section
            Configuration config = ConfigurationManager.OpenExeConfiguration(ConfigurationUserLevel.None);
            //load the model section in the config
            ServiceModelSectionGroup group = ServiceModelSectionGroup.GetSectionGroup(config);

           //search for the wcf-standard xco-section in configuration
            if (group != null)
            {
                foreach (ServiceElement service in group.Services.Services)
                {
                    foreach (ServiceEndpointElement endpoint in service.Endpoints)
                    {
                        if (endpoint.Name == standardConfig)
                        {//found one endpoint with the standard config
                            return endpoint;
                        }
                    }
                }
            }
            return null;
        }


        /// <summary>
        /// Load specific configuration settings.       
        /// </summary>
        /// <param name="coreUri">Baseaddress for the wcf-service</param>
        /// <param name="_spacePortal">The space-portal instance for WCF hosting</param>
        /// <returns>ServiceHost from the SpacePortal</returns>
        private ServiceHost LoadConfiguration(Uri coreUri, IRemoteSpaceService _spacePortal)
        {
            if (LoadEndPointConfig() == null)
            {//Load default configuration for WCF
                return InitDefaultWCFConnection(coreUri, _spacePortal);
            }
            else
            {
                log.Debug("RunServiceHost() - WCF Config.");        
                //create service host with the base address
                return new ServiceHost(_spacePortal, coreUri);
            }
        }

        /// <summary>
        /// Init default wcf-properties
        /// </summary>
        /// <param name="coreUri"></param>
        /// <param name="_spacePortal"></param>
        /// <returns>Default WCF-Service-Host</returns>
        private ServiceHost InitDefaultWCFConnection(Uri coreUri, IRemoteSpaceService _spacePortal)
        {
            log.Debug("RunServiceHost() - No wcf-config found - use default settings");
            NetTcpBinding maxBufferTCP = new NetTcpBinding();   //NetTcpBinding with large buffer sizes
            maxBufferTCP.MaxReceivedMessageSize = 512000;
            maxBufferTCP.MaxBufferSize = 512000;
            maxBufferTCP.ReaderQuotas.MaxArrayLength = 512000;
            maxBufferTCP.Security.Mode = SecurityMode.None;     // no security by default

            if (!string.IsNullOrEmpty(SharedPortName))
            {//enable portsharing, if needed
                maxBufferTCP.PortSharingEnabled = true;
            }

            //Create ServiceHost with default-settings
            ServiceHost sHost = new ServiceHost(_spacePortal);
            sHost.AddServiceEndpoint(
                typeof(IRemoteSpaceService),
                maxBufferTCP, coreUri);
            return sHost;
        }

        /// <summary>
        /// Check if shared port is in use
        /// </summary>
        private void CheckAddressForSharedPort()
        {
            //Uri coreUri = new Uri("net.tcp://" + hostAddress + "/XcoAppSpace");
            if (!string.IsNullOrEmpty(SharedPortName))
            {
                UriBuilder uriB = new UriBuilder();
                uriB.Scheme = address.Scheme;
                uriB.Host = address.Host;
                uriB.Port = address.Port;
                uriB.Path = "/" + SharedPortName + address.AbsolutePath;
                address = uriB.Uri;
            }
        }

        /// <summary>
        /// Method to start in an own thread, that opens a new service host at the given port to receive
        /// messages from remote kernels. The SpacePortal class is used as service.
        /// Note: ServiceHost must run in an own thread, otherwise the host will not work when used in
        /// Windows Forms Applications. This seems to be the case because of some collision with messaging
        /// in WCF and Windows Forms.
        /// </summary>
        private void RunServiceHost()
        {
            CheckAddressForSharedPort();            

            try
            {
                serviceHost = loadHostConfig(address, spacePortal);
             
                if (serviceHost.Description.Endpoints.Count >= 1)
                {
                    bindingHelper.Convert(serviceHost.Description.Endpoints[0].Binding, address.ToString());
                }
                foreach (ServiceEndpoint sep in serviceHost.Description.Endpoints)
                {
                    sep.Behaviors.Add(new XcoWCFEndpointBehavior(this));
                }


                serviceHost.Open();
                running = true;
                log.Debug("Remote Service running.");
                waitForConnect.Set();
            }
            catch (Exception ex)
            {
                //error starting service host -> store exception so it can be thrown in the thread that
                //is waiting for the service host to be opened
                log.Error("Error opening ServiceHost at " + address, ex);
                connectError = new XcoCommunicationException("Error opening ServiceHost at " + address + ": " + ex.Message);
                waitForConnect.Set();
                return;
            }

            //wait for stop
            waitForStop.WaitOne();
            //stop event received -> close service host
            try
            {
				log.Debug("Closing WCF Host...");
                //int subscriberCount = spacePortal.SubscriberCount;
                //log.Info("Close WCF Host with " + subscriberCount + " count");
                serviceHost.Close();

            }
            catch (Exception err)
            {
                log.Error("Close WCF Host Err", err);
                serviceHost.Abort();
            }
            log.Debug("WCF Host closed " + address);
        }

        /// <summary>
        /// Checks if any RemoteSenders have run into a timeout (not sent any message for a 
        /// defined amount of time), and removes these senders and closes their connections.
        /// </summary>
        /// <param name="timerInfo">Field for additional info from the timer that starts the method (is ignored because not additional info needed).</param>
        private void CheckConnectionTimeouts(object timerInfo)
        {
            //timeout - deadline
            DateTime checkDate = DateTime.Now.AddSeconds(-connectionTimeout);

            //keep access time to the remoteSenders Dictionary as short as possible
            List<RemoteSender> timedOut = new List<RemoteSender>();
            List<String> timedOutKeys = new List<String>();
            lock (remoteSenders)
            {
                foreach (String address in remoteSenders.Keys)
                {
                    RemoteSender sender = remoteSenders[address];
                    if (sender.LastSendTime <= checkDate)
                    {//collect all addresses / senders that have timed out
                        timedOut.Add(sender);
                        timedOutKeys.Add(address);
                        log.Debug("Releasing timed out connection to: " + address);
                    }
                }
                //remove RemoteSenders from the list
                foreach (String key in timedOutKeys)
                {
                    remoteSenders.Remove(key);
                }
            }
            //disconnect senders that have timed out
            foreach (RemoteSender sender in timedOut)
            {
                sender.Disconnect();
            }
        }
        private RemoteSender NewRemoteSender(string remoteAddress)
        {
            return new RemoteSender(remoteAddress, log, smsGroup,this);
        }
        /// <summary>
        /// Sends a message to a remote space. If no remote sender for this message exists yet,
        /// a new RemoteSender is created and stored in the list of currently open RemoteSenders.
        /// If a RequestMessage can not be sent, an ResponseMessage with an error is created and given
        /// back to the Request by using the processResponse method.
        /// </summary>
        /// <param name="msg">The message to be sent.</param>
        /// <param name="toAddress">The address where the message should be sent.</param>
        public void Send(XcoMessage msg, string toAddress)
        {
            if (!running)
                msg.RaiseTransferFailedEvent(new XcoCommunicationException("Server for remote communication has not been started!"));
			String remoteAddress = bindingHelper.Convert(toAddress);
            bool newConnection = false;
            try
            {
                RemoteSender sender = null;
                lock (remoteSenders)
                {
                    //find remote sender or create new one, if no sender to this remote address exists
                    if (remoteSenders.ContainsKey(remoteAddress))
                        sender = remoteSenders[remoteAddress];
                    else
                    {
                        sender = createNewRemoteSender(remoteAddress);
                        newConnection = true;
                        remoteSenders.Add(remoteAddress, sender);
                        //immediately call the connect method to initiate the connection for the new sender
                        sender.Connect();
                    }
                }

                //log.Debug("Sending Message to " + remoteAddress);
                //before sending: set remote address of the message to own address, so remote space 
                //knows where the message comes from
				//msg.RemoteAddress = this.LocalAddress; //address.ToString();
                //send message
				sender.SendMessage(msg, LocalAddress);
				msg.RaiseTransferSuccessfulEvent();
            }
            catch (Exception ex)
            {

                //retry opening a new connection if the present connection is not new
                if (!newConnection)
                {
                    try
                    {
                        RemoteSender sender = null;
                        lock (remoteSenders)
                        {
                            if (remoteSenders.ContainsKey(remoteAddress))
                            {
                                sender = remoteSenders[remoteAddress];
								ThreadPool.QueueUserWorkItem(_ => sender.Disconnect()); //wait for disconnect with own thread
                            }
                        }
                    }
                    catch (Exception ex2)
                    {
                        log.Error("Error shutdown old RemoteSender  " + remoteAddress, ex2);
                    }
                    try
                    {
                        log.Warn("Error sending Message to Address: " + remoteAddress + " using the currently open connection. Trying to open new connection.");
                        RemoteSender sender = null;
                        lock (remoteSenders)
                        {
                            sender = createNewRemoteSender(remoteAddress);
                            remoteSenders[remoteAddress] = sender;
                            //immediately call the connect method to initiate the connection for the new sender
                            sender.Connect();
                        }
                        sender.SendMessage(msg, toAddress);
						msg.RaiseTransferSuccessfulEvent();
                    }
                    catch (Exception ex2)
                    {
                        log.Error("Error sending Message to Address: " + remoteAddress, ex2);
						msg.RaiseTransferFailedEvent(new XcoCommunicationException("Error sending Message to Address \"" + remoteAddress + "\" : " + ex2.Message, ex2));
                    }
                }
                else
                {
                    log.Error("Error sending Message to Address: " + remoteAddress, ex);
					msg.RaiseTransferFailedEvent(new XcoCommunicationException("Error sending Message to Address \"" + remoteAddress + "\" : " + ex.Message, ex));
                }
            }
        }

        /// <summary>
        /// Closes the service host, stops the timeout checking and disconnects all senders that 
        /// are currently connected to other spaces.
        /// </summary>
        public void Stop()
        {
            running = false;
			if (timeoutCheckTimer != null)
				timeoutCheckTimer.Dispose();

            ReleaseConnections();

            waitForStop.Set();

            //log.Debug("Remote Service closed.");
        }

        /// <summary>
        /// Closes the service host, stops the timeout checking and disconnects all senders that 
        /// are currently connected to other spaces.
        /// </summary>
        public void Dispose()
        {
            Stop();
        }

        /// <summary>
        /// Release current active connections.
        /// </summary>
        private void ReleaseConnections()
        {
            lock (remoteSenders)
            {
				//log.Debug("Releasing " + remoteSenders.Count + " connections (" + address + ")");
                var list = remoteSenders.Values.ToList();
                foreach (RemoteSender sender in list)
                {
                    sender.Disconnect();
                }
                //remoteSenders.Clear();
            }
        }




        /// <summary>
        /// Close the RemoteSender because the connected channel has closed
        /// </summary>
        /// <param name="channelSessionID">sessionID of closed channel</param>
        public void ShutdownRemoteSender(string channelSessionID)
        {
			RemoteSender sender = null;
			string addr = null;
            lock (remoteSenders)
            {
                foreach (String address in remoteSenders.Keys)
                {
                    //search all remotesenders in the list
                    RemoteSender s = remoteSenders[address];
                    string[] part = s.SessionID.Split(';');
                    string[] part2 = channelSessionID.Split(';');

                    if (part[0] == part2[0]) //compare only uuid
                    {
						sender = s;
						addr = address;
                        break;
                    }
                }
            }

			if (sender != null)
			{
				sender.SendDisable();
				ThreadPool.QueueUserWorkItem(_ => sender.Disconnect());
				remoteSenders.Remove(addr);
				log.Debug("Shutting down remote sender because connected channel has closed: " + address);
			}

        }

        /// <summary>
        /// Remove registered connected channel when closed
        /// </summary>
        /// <param name="sessionID">sessionID of closed channel</param>
        public void Shutdown(string sessionID)
        {
            ShutdownRemoteSender(sessionID);
        }

		internal void RaiseMessageReceivedEvent(XcoMessage msg, string remoteAddress)
		{
			OnMessageReceived(msg, remoteAddress, this);
		}
    }
    
}